/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;

import javax.swing.JEditorPane;
import javax.swing.JTree;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentEvent.EventType;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.model.DocumentContext;
import offset.nodes.client.editor.model.StructureTree;
import offset.nodes.client.veditor.view.VirtualStructureRenderer;

public class VirtualStructureTree extends StructureTree {
    DataModel model = null;
    boolean deferEvents = false;
    class DeferredEvent {
        DocumentEvent.EventType eventType;
        DocumentEvent event;
        
        public DeferredEvent(EventType type, DocumentEvent event) {
            super();
            this.event = event;
            eventType = type;
        }
        
        public DocumentEvent getEvent() {
            return event;
        }
        
        public DocumentEvent.EventType getEventType() {
            return eventType;
        }
    }
    LinkedList deferredEvents = null;
    
    public DataModel getDataModel() {
        if ( model == null )
            model = new DataModel( new DocumentContext(getPane()), getEditorProperties() );
        return model;
    }

    public void userSetCaret() {
           int pos = getPane().getCaretPosition();
           Element element = getDataModel().getRealParagraph(pos);
            selectElement(element);
    }
    
    public class XMLElement implements Element {
        String name;
        String path;
        Element parent = null;
        AttributeSet attributes = null;
        boolean isLeaf = false;
        public XMLElement( String name, String path, Element parent, boolean isLeaf ) {
            this.name = name;
            this.path = path;
            this.parent = parent;
            this.attributes = new SimpleAttributeSet();
            this.isLeaf = isLeaf;
        }
        
        public AttributeSet getAttributes() {
            return attributes;
        }
        
        public Document getDocument() {
            // TODO Auto-generated method stub
            return null;
        }
        
        public Element getElement(int index) {
            // TODO Auto-generated method stub
            return null;
        }
        
        public int getElementCount() {
            // TODO Auto-generated method stub
            return 0;
        }
        
        public int getElementIndex(int offset) {
            // TODO Auto-generated method stub
            return 0;
        }
        
        public int getEndOffset() {
            if ( parent != null )
                return parent.getEndOffset();
            return 0;
        }
        
        public String getName() {
            // TODO Auto-generated method stub
            return name;
        }
        
        public Element getParentElement() {
            return parent;
        }
        
        public int getStartOffset() {
            if ( parent != null )
                return parent.getStartOffset();
            return 0;
        }
        
        public boolean isLeaf() {
            return isLeaf;
        }
        
        public boolean equals( Object element ) {
            if ( !(element instanceof XMLElement))
                return false;
            return name.equals(((XMLElement)element).name);
        }
        
        public String getPath() {
            return path;
        }
        
        @Override
        public String toString() {
            return path;
        }
    }
    
    public static final String ATTRIBUTE_XML = "xml";
    protected HashMap schemas = new HashMap();
    
    public VirtualStructureTree(JEditorPane pane, JTree tree, HashMap editorProperties, Editor editor ) {
        super(pane, tree, editorProperties, editor);
        tree.setCellRenderer(new VirtualStructureRenderer(getDataModel()));
    }
    
    public DefaultMutableTreeNode createTree() {
        Element[] roots = getPane().getDocument().getRootElements();
        
        DefaultMutableTreeNode root = createTree(roots[0]);
        
        return root;
    }
    
    
    protected String getDataName(DefaultMutableTreeNode node) {
        if (node.getUserObject() instanceof Element) {
            Element e = (Element) node.getUserObject();
            if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_DATA)) {
                String data = (String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA);
                return data.substring(data.lastIndexOf("/") + 1);
            } else {
                return ((Element) node.getUserObject()).getName();
            }
        }
        return "";
    }     
    
    protected boolean elementEquals(Element element, DefaultMutableTreeNode treeNode) {
        String nodeName = getName(treeNode);
        if (element instanceof XMLElement)
            nodeName = getDataName(treeNode);
        
        return element.getName().equals(nodeName);
    }
    
    protected Element findParent( DefaultMutableTreeNode node ) {
        if ( node == null || node.getUserObject() == null )
            return null;
        
        return (Element) node.getUserObject();
    }
    
    protected DefaultMutableTreeNode createSchemaTree( Element element, VirtualElement schema ) throws Exception {
        DefaultMutableTreeNode node = createPopupNode( element );
        
        int j=0;
        int i=0;
        Element child = null;
        VirtualElement childSchema = null;
        while( i<schema.getElementCount()) {
            childSchema = (VirtualElement) schema.getElement(i);
            
            if ( j < element.getElementCount())
                child = element.getElement(j);
            else
                child = null;
            
            if ( child != null && child.getName().equals(HTML.Tag.IMPLIED.toString()))
                child = null;
            
            if ( childSchema.isDataElement() ) {
                if ( child != null && getDataModel().getElementName(child).equals( getDataModel().getDataName(childSchema))) {
                    node.add(createSchemaTree( child, childSchema ) );
                    if ( !childSchema.isMultiple())
                        i++;
                    j++;
                } else {
                    String longName = getDataModel().getDataName(childSchema);
                    node.add( createPopupNode( new XMLElement(longName.substring(longName.lastIndexOf("/")+1), longName, //childSchema.getPath(),
                        findParent(node), childSchema.isLeaf() ) ) );
                    i++;
                }
            } else if ( child != null && child.getName().equals(childSchema.getName())){
                node.add(createSchemaTree( child, childSchema));
                i++;
                j++;
            } else {
                /*
                 * This should never happen. However, there seems to be a bug in the JDK. During this stage 
                 * (in the DocumentEvent processing queue), not everything has its end state. Sometimes the element structure
                 * gets confused. So far, this seems always to deal with additional "ghost" elements.
                 * These cannot be matched against a corresponding virtual element. So we just skip the elements, until a match can be found
                 * or we have consumed all elements.
                 */
                while(j+1 < element.getElementCount() && !child.getName().equals(childSchema.getName())) {
                    j++;
                    child = element.getElement(j);
                }
                if( child != null && child.getName().equals(childSchema.getName())) {
                    node.add(createSchemaTree(child, childSchema));
                    j++;
                }
                i++; 
            }
            
        }
        
        return node;
    }
    
    protected DefaultMutableTreeNode createTree( Element element )  {
        if (getModel().isInlineElement(element))
            element = getModel().getInlineHTMLELement(element);
        
        DefaultMutableTreeNode node = createPopupNode( element );
        
        try {
            for( int i=0; i<element.getElementCount(); i++) {
                Element child = element.getElement(i);
                
                if (getModel().isArtificialElement(child)) {
                    for (int j=0; j<child.getElementCount(); j++) {
                        if (!isInvisible(child.getElement(j)))
                            node.add(createTree(child.getElement(j)));
                    }
                    
                    continue;
                }
                
                if ( getDataModel().isTemplateElement(child)) {
                    Element templateElement = child;
                    VirtualBlock vb = getDataModel().getVirtualBlock(child);
                    if ( vb != null) {
                        node.add( createSchemaTree( templateElement, vb.getRoot() ) );
                        continue;
                    }                    
                }
                
                if ( !isInvisible(child))
                    node.add( createTree(child));
            }
        } catch( Exception e) {
            e.printStackTrace();
        }
        
        
        return node;
    }
    
    protected boolean isLastChild(Element element ) {
        return element == element.getParentElement().getElement(element.getParentElement().getElementCount()-1);
    }
    
    public void updateTree( DocumentEvent.EventType type, DefaultMutableTreeNode parentNode, Element parentElement ) {
        if ( parentNode.getChildCount() == 0 ) {
            if ( parentElement.getElementCount() > 0)
                for( int e=0; e<parentElement.getElementCount(); e++) {
                Element childElement = parentElement.getElement(e);
                if ( isInvisible(childElement)) {
                    e++;
                    continue;
                }
                
                ((DefaultTreeModel)tree.getModel()).insertNodeInto(createPopupNode(parentElement.getElement(e)), parentNode, e);
                }
            return;
        }
        
        DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) parentNode.getFirstChild();
        int e=0;
        int n=0;
        while( childNode != null) {
            if ( parentElement.getElementCount() < e+1) {
                DefaultMutableTreeNode oldNode = childNode;
                childNode = childNode.getNextSibling();
                ((DefaultTreeModel) tree.getModel()).removeNodeFromParent(oldNode);
                continue;
            }
            Element childElement = parentElement.getElement(e);
            if ( isInvisible(childElement)) {
                e++;
                continue;
            }
            
            if ( childNode.getUserObject() != childElement ) {
                Element before = (Element) childNode.getUserObject();
                if ( before.getName().equals( childElement.getName()))
                    childNode.setUserObject(childElement);
                else {
                    if ( type == DocumentEvent.EventType.INSERT) {
                        DefaultMutableTreeNode newNode = createTree(childElement);
                        ((DefaultTreeModel) tree.getModel()).insertNodeInto(newNode,
                            parentNode, n);
                        e++;
                        continue;
                    } else if ( type == DocumentEvent.EventType.REMOVE) {
                        DefaultMutableTreeNode oldNode = childNode;
                        childNode = childNode.getNextSibling();
                        ((DefaultTreeModel) tree.getModel()).removeNodeFromParent(oldNode);
                        continue;
                    }
                }
            }
            
            updateTree( type, childNode, childElement );
            
            e++;
            n++;
            childNode = childNode.getNextSibling();
        }
        
        while( e < parentElement.getElementCount() ) {
            Element childElement = parentElement.getElement(e);
            if ( isInvisible(childElement) ) {
                e++;
                continue;
            }
            
            DefaultMutableTreeNode newNode = createTree(childElement);
            ((DefaultTreeModel) tree.getModel()).insertNodeInto(newNode,
                parentNode, n);
            
            n++;
            e++;
        }
        refresh();
    }
    
    public void updateTree(DocumentEvent.EventType type, DocumentEvent e) {
        if ( e.getLength() == 1 && e.toString().indexOf("ElementEdit") < 0)
            return;
        
        DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) tree.getModel().getRoot();
        Element rootElement = getPane().getDocument().getRootElements()[0];
        
        DefaultMutableTreeNode newRootNode = createTree(rootElement);
        updateTree( type, rootNode, newRootNode );
    }
    
    public void changedUpdate(DocumentEvent e) {
        if ( deferEvents ) {
            deferredEvents.add( new DeferredEvent(DocumentEvent.EventType.CHANGE,e));
            return;
        }
        updateTree(DocumentEvent.EventType.CHANGE,e);
    }
    
    public void insertUpdate(DocumentEvent e) {
        if ( deferEvents ) {
            deferredEvents.add( new DeferredEvent(DocumentEvent.EventType.INSERT,e));
            return;
        }
        updateTree(DocumentEvent.EventType.INSERT,e);
    }
    
    public void removeUpdate(DocumentEvent e) {
        if ( deferEvents ) {
            deferredEvents.add( new DeferredEvent(DocumentEvent.EventType.REMOVE,e));
            return;
        }
        updateTree(DocumentEvent.EventType.REMOVE,e);
    }
    
    public void ignoreEvents( boolean defer ) {
        if ( deferEvents == false )
            deferredEvents = new LinkedList();
        else {
            Iterator i = deferredEvents.iterator();
            while( i.hasNext()) {
                DeferredEvent event = (DeferredEvent) i.next();
                updateTree( event.getEventType(), event.getEvent());
            }
            deferredEvents = null;
        }
        
        this.deferEvents = defer;
    }
    
}
